package mrpanyu.guitool.base.annotation;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import mrpanyu.guitool.base.model.Action;
import mrpanyu.guitool.base.model.Parameter;
import mrpanyu.guitool.base.model.Tool;
import mrpanyu.guitool.base.model.ToolModel;

public class AnnotatedToolModel extends ToolModel {

	protected Class<?> modelClass;
	protected Object model;
	protected Map<String, FieldAndAnnotation> parameterBindings = new LinkedHashMap<>();
	protected Map<String, MethodAndAnnotation> actionBindings = new LinkedHashMap<>();
	protected Map<String, Method> parameterChangeBindings = new LinkedHashMap<>();
	protected Method onloadMethod;

	public AnnotatedToolModel(Class<?> modelClass) {
		try {
			this.modelClass = modelClass;
			this.model = modelClass.newInstance();
			parseClass();
			parseFields();
			parseMethods();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void parseClass() {
		mrpanyu.guitool.base.annotation.ToolModel toolModelA = modelClass
				.getAnnotation(mrpanyu.guitool.base.annotation.ToolModel.class);
		if (toolModelA == null) {
			throw new RuntimeException("Class " + modelClass.getName() + " is not annotated with @ToolModel");
		}
		this.setName(modelClass.getName());
		if (toolModelA.displayName().length() == 0) {
			this.setDisplayName(modelClass.getSimpleName());
		} else {
			this.setDisplayName(toolModelA.displayName());
		}
		this.setDescription(toolModelA.description());
		this.setEnableProfiles(toolModelA.enableProfiles());
	}

	private void parseFields() throws Exception {
		List<Class<?>> classes = getAllClasses();
		for (Class<?> cls : classes) {
			Field[] declaredFields = cls.getDeclaredFields();
			for (Field f : declaredFields) {
				mrpanyu.guitool.base.annotation.Parameter annotation = f
						.getAnnotation(mrpanyu.guitool.base.annotation.Parameter.class);
				if (annotation != null) {
					FieldAndAnnotation fa = new FieldAndAnnotation();
					fa.field = f;
					fa.annotation = annotation;
					parameterBindings.put(f.getName(), fa);
				}
			}
		}
		List<FieldAndAnnotation> faList = new ArrayList<>(parameterBindings.values());
		faList.sort((fa1, fa2) -> Integer.compare(fa1.annotation.order(), fa2.annotation.order()));
		for (FieldAndAnnotation fa : faList) {
			Field f = fa.field;
			mrpanyu.guitool.base.annotation.Parameter annotation = fa.annotation;
			Parameter parameter = new Parameter();
			parameter.setName(f.getName());
			if (annotation.displayName().length() == 0) {
				parameter.setDisplayName(f.getName());
			} else {
				parameter.setDisplayName(annotation.displayName());
			}
			parameter.setType(annotation.type());
			String value = getModelParamValue(f.getName());
			parameter.setValue(value);
			parameter.setDescription(annotation.description());
			parameter.setOptions(annotation.options());
			parameter.setVisible(annotation.visible());
			parameter.setChangeHandler(this::onParameterChange);
			this.addParameter(parameter);
		}
	}

	private void parseMethods() {
		List<Class<?>> classes = getAllClasses();
		for (Class<?> cls : classes) {
			Method[] declaredMethods = cls.getDeclaredMethods();
			for (Method m : declaredMethods) {
				OnParameterChange onParameterChangeA = m.getAnnotation(OnParameterChange.class);
				if (onParameterChangeA != null) {
					for (String value : onParameterChangeA.value()) {
						parameterChangeBindings.put(value, m);
					}
				}
				mrpanyu.guitool.base.annotation.Action actionA = m
						.getAnnotation(mrpanyu.guitool.base.annotation.Action.class);
				if (actionA != null) {
					MethodAndAnnotation ma = new MethodAndAnnotation();
					ma.method = m;
					ma.annotation = actionA;
					actionBindings.put(m.getName(), ma);
				}
				OnLoad onloadA = m.getAnnotation(OnLoad.class);
				if (onloadA != null) {
					onloadMethod = m;
					this.setOnloadHandler(t -> {
						onloadMethod.invoke(model, t);
					});
				}
			}
		}
		List<MethodAndAnnotation> maList = new ArrayList<>(actionBindings.values());
		maList.sort((ma1, ma2) -> Integer.compare(ma1.annotation.order(), ma2.annotation.order()));
		for (MethodAndAnnotation ma : maList) {
			Method m = ma.method;
			mrpanyu.guitool.base.annotation.Action annotation = ma.annotation;
			Action action = new Action();
			action.setName(m.getName());
			if (annotation.displayName().length() == 0) {
				action.setDisplayName(m.getName());
			} else {
				action.setDisplayName(annotation.displayName());
			}
			action.setHandler(t -> {
				this.onAction(m.getName(), t);
			});
			action.setDescription(annotation.description());
			action.setVisible(annotation.visible());
			this.addAction(action);
		}
	}

	private List<Class<?>> getAllClasses() {
		List<Class<?>> classes = new ArrayList<>();
		Class<?> cls = modelClass;
		while (!Object.class.equals(cls)) {
			classes.add(cls);
			cls = cls.getSuperclass();
		}
		Collections.reverse(classes);
		return classes;
	}

	private void onParameterChange(Parameter p, Tool tool) {
		try {
			viewToModel(tool);
			Method m = parameterChangeBindings.get(p.getName());
			if (m != null) {
				invokeHandlerMethod(m, tool);
			}
			modelToView();
		} catch (InvocationTargetException e) {
			e.getTargetException().printStackTrace();
			tool.errorMessage(e.getTargetException());
		} catch (Throwable t) {
			t.printStackTrace();
			tool.errorMessage(t);
		}
	}

	private void onAction(String actionName, Tool tool) {
		try {
			viewToModel(tool);
			Method m = actionBindings.get(actionName).method;
			if (m != null) {
				invokeHandlerMethod(m, tool);
			}
			modelToView();
		} catch (InvocationTargetException e) {
			e.getTargetException().printStackTrace();
			tool.errorMessage(e.getTargetException());
		} catch (Throwable t) {
			t.printStackTrace();
			tool.errorMessage(t);
		}
	}

	private String getModelParamValue(String name) throws Exception {
		Field f = parameterBindings.get(name).field;
		f.setAccessible(true);
		Object value = f.get(model);
		if (value == null) {
			return "";
		} else if (value instanceof File) {
			return ((File) value).getCanonicalPath();
		} else {
			return value.toString();
		}
	}

	private void setModelParamValue(String name, String value, Tool tool) throws Exception {
		Field f = parameterBindings.get(name).field;
		f.setAccessible(true);
		Class<?> type = f.getType();
		if (value == null) {
			f.set(model, null);
		} else if (File.class.equals(type)) {
			if (value.trim().length() == 0) {
				f.set(model, null);
			} else {
				f.set(model, new File(value));
			}
		} else if (Integer.TYPE.equals(type) || Integer.class.equals(type)) {
			try {
				Integer intValue = Integer.valueOf(value, 10);
				f.set(model, intValue);
			} catch (Exception e) {
				tool.warnMessage("Cannot parse parameter: " + name + " for value: " + value);
			}
		} else if (Long.TYPE.equals(type) || Long.class.equals(type)) {
			try {
				Long longValue = Long.valueOf(value, 10);
				f.set(model, longValue);
			} catch (Exception e) {
				tool.warnMessage("Cannot value: " + value + " for parameter: " + name);
			}
		} else if (Float.TYPE.equals(type) || Float.class.equals(type)) {
			try {
				Float floatValue = Float.valueOf(value);
				f.set(model, floatValue);
			} catch (Exception e) {
				tool.warnMessage("Cannot value: " + value + " for parameter: " + name);
			}
		} else if (Double.TYPE.equals(type) || Double.class.equals(type)) {
			try {
				Double doubleValue = Double.valueOf(value);
				f.set(model, doubleValue);
			} catch (Exception e) {
				tool.warnMessage("Cannot value: " + value + " for parameter: " + name);
			}
		} else if (BigDecimal.class.equals(type)) {
			try {
				BigDecimal numValue = new BigDecimal(value);
				f.set(model, numValue);
			} catch (Exception e) {
				tool.warnMessage("Cannot value: " + value + " for parameter: " + name);
			}
		} else if (String.class.equals(type)) {
			f.set(model, value);
		} else {
			tool.warnMessage("Unsupported parameter type: " + type + " for parameter: " + name);
		}
	}

	private void invokeHandlerMethod(Method m, Tool tool) throws Exception {
		Class<?>[] parameterTypes = m.getParameterTypes();
		Object[] params = new Object[parameterTypes.length];
		for (int i = 0; i < parameterTypes.length; i++) {
			if (Tool.class.equals(parameterTypes[i])) {
				params[i] = tool;
			}
		}
		m.invoke(model, params);
	}

	private void viewToModel(Tool tool) throws Exception {
		List<Parameter> parameters = getParameters();
		for (Parameter param : parameters) {
			setModelParamValue(param.getName(), param.getValue(), tool);
		}
	}

	private void modelToView() throws Exception {
		List<Parameter> parameters = getParameters();
		for (Parameter param : parameters) {
			String value = getModelParamValue(param.getName());
			param.setValue(value);
		}
	}

	private class FieldAndAnnotation {
		Field field;
		mrpanyu.guitool.base.annotation.Parameter annotation;
	}

	private class MethodAndAnnotation {
		Method method;
		mrpanyu.guitool.base.annotation.Action annotation;
	}

}
